package com.jp.fm;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.math.BigInteger;
import java.net.URL;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.TreeMap;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;

import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.ObjectExistsException;
import net.sf.ehcache.Statistics;
import net.sf.ehcache.config.CacheConfiguration;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import org.jasypt.util.numeric.BasicIntegerNumberEncryptor;
import org.jasypt.util.text.BasicTextEncryptor;

import com.jp.fm.dao.AccountCacheListener;
import com.jp.fm.dao.AccountDao;
import com.jp.fm.dao.CategoryDao;
import com.jp.fm.data.Account;
import com.jp.fm.data.AccountType;
import com.jp.fm.data.Site;
import com.jp.fm.data.Transaction;
import com.jp.fm.data.UserAccount;
import com.jp.fm.service.CategoryService;
import com.jp.fm.service.TransactionService;

public final class HomeFinanceRunner {

	private static final int MAX_CACHE_ENTRIES = 500;
	private static final int HOURS_TO_DAYS = 24;
	private static final int MINUTES_TO_HOURS = 60;
	private static final int SECONDS_TO_MINUTES = 60;
	private static final int MILLISECONDS_TO_SECONDS = 1000;
	public static final int DB_COLUMN_FIRST = 1;
	public static final int DB_COLUMN_SECOND = 2;
	public static final int DB_COLUMN_THIRD = 3;
	public static final int DB_COLUMN_FOURTH = 4;
	public static final int DB_COLUMN_FIFTH = 5;
	public static final int DB_COLUMN_SIXTH = 6;
	public static final int DB_COLUMN_SEVENTH = 7;
	public static final int DB_COLUMN_EIGHTH = 8;
	public static final int DB_COLUMN_NINETH = 9;

	static {
		try {
			Class.forName("org.hsqldb.jdbcDriver");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}

	private static CacheManager cacheManager = new CacheManager();

	public static CacheManager getCacheManager() {
		return cacheManager;
	}

	public static Cache getCache(final String cacheName) {

		Cache cache = cacheManager.getCache(cacheName);

		if (null == cache) {
			try {
				cache = registerCache(cacheName);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		return cache;
	}

	private static Cache registerCache(final String cacheName) throws Exception {

		Cache cache = null;

		try {
			cache = new Cache(new CacheConfiguration(cacheName,
					MAX_CACHE_ENTRIES));
			if ("Account".equals(cacheName)) {
				cache.getCacheEventNotificationService().registerListener(
						new AccountCacheListener());
			}
			cacheManager.addCache(cache);
			cache.setStatisticsEnabled(true);
			cache.setStatisticsAccuracy(Statistics.STATISTICS_ACCURACY_GUARANTEED);
		} catch (ObjectExistsException e) {
			throw new Exception("Cache named " + cacheName + " already exists!");
		}

		return cache;
	}

	private HomeFinanceRunner() {
	}

	/**
	 * @param args
	 */
	public static void main(final String[] args) {

		// Used for testing only
	}

	public static void refreshAccountFromSource(final Account acct,
			final CategoryService categoryService,
			final CategoryDao categoryDao, final AccountDao accountDao) {
		UserAccount uAcct = UserAccount.findByName(acct.getName());
		if (null != uAcct) {
			String runClass = uAcct.getRunClass();
			Object runClassObj = null;
			try {
				runClassObj = Class.forName(runClass).newInstance();
			} catch (Exception e1) {
				e1.printStackTrace();
			}

			if (null != runClassObj) {
				try {
					String simpleName = runClassObj.getClass().getSimpleName();

					Method setUpMethod = Class.forName(runClass).getMethod(
							"setUp", (Class[]) null);
					setUpMethod.invoke(runClassObj, (Object[]) null);

					Method setMethod = Class.forName(runClass).getMethod(
							"setAccount", new Class[] { Account.class });
					setMethod.invoke(runClassObj, new Object[] { acct });

					setMethod = Class.forName(runClass).getMethod(
							"setCategoryService",
							new Class[] { CategoryService.class });
					setMethod.invoke(runClassObj,
							new Object[] { categoryService });

					setMethod = Class.forName(runClass)
							.getMethod("setCategoryDao",
									new Class[] { CategoryDao.class });
					setMethod.invoke(runClassObj, new Object[] { categoryDao });

					setMethod = Class.forName(runClass).getMethod(
							"setAccountDao", new Class[] { AccountDao.class });
					setMethod.invoke(runClassObj, new Object[] { accountDao });

					Method testMethod = Class.forName(runClass).getMethod(
							"test" + simpleName, (Class[]) null);
					testMethod.invoke(runClassObj, (Object[]) null);

				} catch (Exception e) {
					e.printStackTrace();

					// Record failure
					acct.setLastFailureDate(new Timestamp(new Date().getTime()));
					accountDao.writeObject(acct);
					HomeFinanceRunner.getCache(AccountDao.CACHE_NAME)
							.removeAll();
				}

				// Try running teardown to close the browser
				Method tearDownMethod;
				try {
					tearDownMethod = Class.forName(runClass).getMethod(
							"tearDown", (Class[]) null);

					tearDownMethod.invoke(runClassObj, (Object[]) null);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		} else {
			System.out.println("UserAccount not found for account named "
					+ acct.getName());
		}
	}

	public static class ReverseFloatComparator implements Comparator<Float> {

		@Override
		public final int compare(final Float o1, final Float o2) {

			return o2.compareTo(o1);
		}

	}

	public static Map<AccountType, List<Account>> getAccountsByType() {
		List<Account> accountList = null;
		Map<AccountType, List<Account>> accountsByType = new TreeMap<AccountType, List<Account>>();
		AccountDao accountDao = new AccountDao();
		List<AccountType> ats = AccountType.findAll();

		Collections.sort(ats);

		for (Iterator<AccountType> iterator = ats.iterator(); iterator
				.hasNext();) {
			AccountType accountType = iterator.next();

			List<Account> accts = accountDao.findByType(accountType);
			if (null != accts && accts.size() > 0) {
				accountList = new ArrayList<Account>();
				accountList.addAll(accts);
				Collections.sort(accountList, new Comparator<Account>() {
					public int compare(final Account f1, final Account f2) {
						return f1.getSequence().compareTo(f2.getSequence());
					}
				});
				accountsByType.put(accountType, accountList);
			}
		}

		return accountsByType;
	}

	public static String getLastUpdateText(final java.sql.Timestamp ts) {

		String ret = null;

		Date now = new Date();

		// check days
		int daysBetween = daysBetween(ts, now);

		if (daysBetween < 0) {
			daysBetween *= -1;
		}

		if (daysBetween <= 0) {

			// check hours
			int hoursBetween = hoursBetween(ts, now);

			if (hoursBetween < 0) {
				hoursBetween *= -1;
			}

			if (hoursBetween <= 0) {

				int minutesBetween = minutesBetween(ts, now);

				if (minutesBetween < 0) {
					minutesBetween *= -1;
				}

				if (minutesBetween <= 0) {
					ret = " a short time";
				} else {
					ret = minutesBetween + " minute";
					if (minutesBetween > 1) {
						ret += "s";
					}
				}

			} else {
				ret = hoursBetween + " hour";
				if (hoursBetween > 1) {
					ret += "s";
				}
			}
		} else {

			ret = daysBetween + " day";
			if (daysBetween > 1) {
				ret += "s";
			}
		}

		return ret;
	}

	public static int daysBetween(final Date d1, final Date d2) {
		return (int) ((d2.getTime() - d1.getTime()) / (MILLISECONDS_TO_SECONDS
				* SECONDS_TO_MINUTES * MINUTES_TO_HOURS * HOURS_TO_DAYS));
	}

	public static int hoursBetween(final Date d1, final Date d2) {
		return (int) ((d2.getTime() - d1.getTime()) / (MILLISECONDS_TO_SECONDS
				* SECONDS_TO_MINUTES * MINUTES_TO_HOURS));
	}

	public static int minutesBetween(final Date d1, final Date d2) {
		return (int) ((d2.getTime() - d1.getTime()) / (MILLISECONDS_TO_SECONDS * SECONDS_TO_MINUTES));
	}

	public static void exportTransactionsByAccount(final String acct)
			throws IOException, ParseException {

		final SimpleDateFormat sdfFileName = new SimpleDateFormat(
				"MM_dd_yyyy_hh_mm__sss");
		final SimpleDateFormat sdf = new SimpleDateFormat("MM/dd/yyyy");

		StringBuffer sb = new StringBuffer();

		TransactionService transactionService = new TransactionService();

		List<Transaction> trans = transactionService.searchTransactionsByDate(
				sdf.parse("01/01/2000"), new Date(), acct);

		// write the transactions to a CSV file
		File outFile = new File(acct + "_transactions_"
				+ sdfFileName.format(new Date()) + ".csv");
		for (Iterator<Transaction> iterator = trans.iterator(); iterator
				.hasNext();) {
			Transaction transaction = iterator.next();

			sb.append(sdf.format(transaction.getDate())).append(",")
					.append(transaction.getAccount()).append(",")
					.append(transaction.getAmount()).append(",")
					.append(transaction.getCategory().getName()).append("\n");

			FileUtils.writeStringToFile(outFile, sb.toString());
		}

	}

	public static void closeConnection(final Connection c,
			final PreparedStatement p, final ResultSet rs) {

		if (null != rs) {
			try {
				rs.close();
			} catch (SQLException e) {

				e.printStackTrace();
			}
		}

		if (null != p) {
			try {
				p.close();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}

		if (null != c) {
			try {
				c.close();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}
	}

	public static Connection getConnection() throws SQLException, IOException {

		Connection con = null;

		try {
			Context initContext = new InitialContext();
			Context envContext = (Context) initContext.lookup("java:/comp/env");
			DataSource ds = (DataSource) envContext.lookup("/jdbc/HFMDB");
			con = ds.getConnection();
		} catch (Throwable e) {

			// e.printStackTrace();
			System.out
					.println("Unable to get a DB connection from the pool so creating one...");

			// Load property file with connection info
			Properties properties = new Properties();
			properties.load(HomeFinanceRunner.class
					.getResourceAsStream("/db.properties"));

			// Get the test DB file location
			String dbFileLoc = properties.getProperty("dbfileloc");

			// Find the DB file on the classpath
			URL fileUrl = HomeFinanceRunner.class.getResource(dbFileLoc);
			if (null == fileUrl) {
				// try absolute path
				File absPath = new File(dbFileLoc);
				fileUrl = absPath.toURI().toURL();
			}
			File dbFile = FileUtils.toFile(fileUrl);

			// get the absolute path and file name without extension
			String dbAbsPathNoExt = dbFile.getParent() + File.separatorChar
					+ FilenameUtils.removeExtension(dbFile.getName());

			String dbUrl = properties.getProperty("url");
			// Replace the place holder with the file location on disk
			dbUrl = StringUtils.replace(dbUrl, "#dbfileloc#", dbAbsPathNoExt);

			// Get Connection
			con = DriverManager.getConnection(dbUrl,
					properties.getProperty("username"),
					properties.getProperty("password"));
		}
		return con;
	}

	public static String removeEnclosingQuotes(final String str) {

		String ret = null;

		if (null != str && str.startsWith("\"") && str.endsWith("\"")) {
			ret = str.substring(1, str.length() - 1);
		} else {
			ret = str;
		}

		return ret;
	}

	public static int convertBoolToInt(final boolean bool) {
		int ret = 1;
		if (!bool) {
			ret = 0;
		}

		return ret;
	}

	public static boolean convertIntToBool(final int i) {
		boolean ret = false;
		if (i == 1) {
			ret = true;
		}

		return ret;
	}

	protected static void encryptDecryptDb(final boolean encrypt)
			throws IOException {

		Connection c = null;
		PreparedStatement p = null;
		ResultSet rs = null;

		// Get config
		InputStream inStream = TestHelper.class
				.getResourceAsStream("/encrypt_config.properties");
		Properties props = new Properties();
		props.load(inStream);

		String tables = props.getProperty("tables");

		// for each table lookup the columns to be encrytped
		StringTokenizer tableTokenizer = new StringTokenizer(tables, ",");
		String table = null;
		String column = null;
		StringBuffer sql = null;
		Map<String, java.lang.reflect.Type> columnMap = null;
		Map<Integer, Map<String, Object>> columnValueMap = null;
		Map<String, Object> valueMap = null;
		while (tableTokenizer.hasMoreTokens()) {

			sql = new StringBuffer("select ");
			columnMap = new HashMap<String, Type>();
			columnValueMap = new HashMap<Integer, Map<String, Object>>();

			table = tableTokenizer.nextToken();

			// Create SELECT sql
			StringTokenizer columnTokenizer = new StringTokenizer(
					props.getProperty(table), ",");
			while (columnTokenizer.hasMoreTokens()) {
				column = columnTokenizer.nextToken();
				StringTokenizer typeTokenizer = new StringTokenizer(column, ":");
				column = typeTokenizer.nextToken();
				String classTypeStr = typeTokenizer.nextToken();
				Class classType = null;
				try {
					classType = ClassUtils.getClass(classTypeStr);
				} catch (ClassNotFoundException e) {
					e.printStackTrace();
				}

				sql.append(column).append(", ");
				columnMap.put(column, classType);
			}

			sql.append(" id from ").append(table);

			try {
				c = HomeFinanceRunner.getConnection();
				p = c.prepareStatement(sql.toString());
				rs = p.executeQuery();

				while (rs.next()) {
					valueMap = new HashMap<String, Object>();
					for (String col : columnMap.keySet()) {
						if (String.class.equals(columnMap.get(col))) {
							valueMap.put(col, rs.getString(col));
						} else if (Integer.class.equals(columnMap.get(col))) {
							valueMap.put(col, rs.getInt(col));
						}

					}
					columnValueMap.put(rs.getInt("ID"), valueMap);
				}
			} catch (SQLException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			} finally {
				HomeFinanceRunner.closeConnection(c, p, rs);
			}

			System.out.println(table);
			System.out.println(columnValueMap);

			if (encrypt) {
				// encrypt values
				encryptMap(columnValueMap);
			} else {
				// decrypt values
				decryptMap(columnValueMap);
			}
			System.out.println(columnValueMap);

			updateDbWithValues(table, columnValueMap);
		}

		Site site = Site.findSite();
		site.setDbEncrypted(encrypt);
		site.writeObject();
	}

	protected static void updateDbWithValues(final String table,
			final Map<Integer, Map<String, Object>> map) {

		Connection c = null;
		PreparedStatement p = null;
		ResultSet rs = null;

		StringBuffer sql = null;

		Map<String, Object> valueMap = null;
		Iterator<Integer> it = map.keySet().iterator();
		while (it.hasNext()) {
			sql = new StringBuffer("update " + table + " set ");
			Integer key = (Integer) it.next();

			valueMap = map.get(key);
			Iterator<String> valueIt = valueMap.keySet().iterator();
			while (valueIt.hasNext()) {
				String valueKey = (String) valueIt.next();
				Object value = valueMap.get(valueKey);
				if (value instanceof String) {
					value = ((String) value).replaceAll("'", "''");
					sql.append(valueKey + " = '" + value + "', ");
				} else if (value instanceof BigInteger) {
					sql.append(valueKey + " = " + value + ", ");
				}
			}

			sql.delete(sql.length() - 2, sql.length());
			sql.append(" where id = " + key);

			try {
				c = HomeFinanceRunner.getConnection();
				System.out.println(sql.toString());
				p = c.prepareStatement(sql.toString());

				p.executeUpdate();
				c.commit();

			} catch (SQLException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			} finally {
				HomeFinanceRunner.closeConnection(c, p, rs);
			}
		}
	}

	protected static void encryptMap(final Map<Integer, Map<String, Object>> map) {

		Object encryptedObj = null;
		Map<String, Object> valueMap = null;
		Iterator<Integer> it = map.keySet().iterator();
		while (it.hasNext()) {
			Integer key = (Integer) it.next();
			valueMap = map.get(key);
			Iterator<String> valueIt = valueMap.keySet().iterator();
			while (valueIt.hasNext()) {
				String valueKey = (String) valueIt.next();
				Object value = valueMap.get(valueKey);
				encryptedObj = encryptValue(value);
				valueMap.put(valueKey, encryptedObj);
			}
		}
	}

	protected static Object encryptValue(Object value) {

		Object ret = null;
		Site site = Site.findSite();
		BasicTextEncryptor textEncryptor = new BasicTextEncryptor();
		BasicIntegerNumberEncryptor intEncryptor = new BasicIntegerNumberEncryptor();
		textEncryptor.setPassword(site.getSiteKey());
		intEncryptor.setPassword(site.getSiteKey());

		if (value instanceof String) {
			ret = textEncryptor.encrypt((String) value);
		} else if (value instanceof Integer) {
			ret = intEncryptor.encrypt(BigInteger.valueOf((Integer) value));
		}

		return ret;
	}

	protected static void decryptMap(final Map<Integer, Map<String, Object>> map) {
		Site site = Site.findSite();
		BasicTextEncryptor textEncryptor = new BasicTextEncryptor();
		BasicIntegerNumberEncryptor intEncryptor = new BasicIntegerNumberEncryptor();
		textEncryptor.setPassword(site.getSiteKey());
		intEncryptor.setPassword(site.getSiteKey());
		Object encryptedObj = null;
		Map<String, Object> valueMap = null;
		Iterator<Integer> it = map.keySet().iterator();
		while (it.hasNext()) {
			Integer key = (Integer) it.next();
			valueMap = map.get(key);
			Iterator<String> valueIt = valueMap.keySet().iterator();
			while (valueIt.hasNext()) {
				String valueKey = (String) valueIt.next();
				Object value = valueMap.get(valueKey);
				if (value instanceof String) {
					encryptedObj = textEncryptor.decrypt((String) value);
				} else if (value instanceof Integer) {
					encryptedObj = intEncryptor.decrypt(BigInteger
							.valueOf((Integer) value));
				}
				valueMap.put(valueKey, encryptedObj);
			}
		}
	}

}
